#!/usr/bin/python
# -*- coding: utf-8 -*-

# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>, and others
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.

import sys
import datetime
import traceback
import re
import shlex
import os

DOCUMENTATION = '''
---
module: pacemaker
version_added: historical
short_description: Manage pacemaker cluster resources
description:
  Manage pacemaker cluster resources through the crm command.
options:
  action:
    description:
      The requested action. Allowed actions:
          - commit
          - primitive
          - monitor
          - group
          - clone
          - ms
          - rsc_template
          - location
          - colocation
          - order
          - property
          - rsc_defaults
          - fencing_topology
    required: true
    default: null
  shadow:
    description:
      Use a shadow cib for the operations
    required: false
    default: null
  name:
    description:
      The resource name where applicable
    required: false
    default: null
  state:
    description:
      - Indicate desired state of the resource
    choices: ['present', 'absent']
    default: present
  params:
    description: the various parameters for the resource

notes: Currently only property, primitive, colocation and ms resources are tested.
author: Akira Yoshiyama, Gyorgy Szombathelyi
'''

EXAMPLES = '''
# Define a property
- pacemaker: action='property' params='stonith-enabled=false'

# Define a primitive
- pacemaker:
    action: primitive
    name: vip
    params:
      type: ocf:heartbeat:IPaddr2
      meta:
         target-role: Started
      params:
        ip: 192.168.0.250
        cidr_netmask: "24"
        nic: eth0
    state: present

# Define a master-slave resource
- pacemaker:
    action: ms
    name: ms_galera
    params:
      rsc: p_galera
      meta:
         target-role: Started
         master-max: "3"

# Define a colocation
- pacemaker:
    action: colocation
    name: colocate_vip
    params:
      score: inf
      rsc:
        - mgmt_vip
        - public_vip
'''


REX = re.compile(
    r"""([^ ]+)(=(['"])((?:\\.|(?!\3).)*)\3|)""",
    re.DOTALL | re.VERBOSE)

def unquote(str):
    if str[0]==str[-1] and str[0] in '\'\"':
        return str[1:-1]
    else:
        return str

def _stringify_dict(src):
    ret = {}
    for (key, value) in src.iteritems():
       if isinstance(value, dict):
           ret[key] = _stringify_dict(value)
       elif isinstance(value, int):
           ret[key] = str(value)
       else:
           ret[key] = value
    return ret


class BaseParser(object):
    id_name = None
    id = None
    partial_compare = False
    module = None

    def __init__(self, args, module=None):
        if module:
            self.module = module
        self.cib = self.parse(args)
        self.command = self.cib["command"]
        if self.id_name:
            self.id = self.cib[self.id_name]

    def parse(self, args):
        raise NotImplementedError()

    def is_same(self, obj):
        if len(obj) + 2 != len(self.cib) and not self.partial_compare:
            return False
        for key, value in obj.iteritems():
            if key not in self.cib.keys():
                return False
            if value != self.cib.get(key):
                return False
        return True


class PrimitiveParser(BaseParser):
    id_name = "rsc"

    def parse(self, args):
        ret = dict(
            command=args.pop(0),
            rsc=args.pop(0),
            type=args.pop(0),
            )
        mode = None
        while (len(args) > 0):
            arg = unquote(args.pop(0))
            if (arg in ["params", "meta", "utilization",
                        "operations", "op"]):
                mode = arg
                if (arg == "op"):
                    op_type = args.pop(0)
                continue
    
            if '=' not in arg:
                raise Exception("no key=value option: %s" % arg)

            key, value = arg.split("=")
            if key == "":
                self.module.fail_json(
                    rc=258, msg="no key in key=value option")
            if value == "":
                self.module.fail_json(
                    rc=258,
                    msg="no value in key=value option (key=%s)" % key)

            if mode not in ret:
                ret[mode] = {}
            if (mode == "op"):
                if op_type not in ret[mode]:
                    ret[mode][op_type] = {}
                ret[mode][op_type][key] = unquote(value)
            else:
                if mode not in ret:
                    ret[mode] = {}
                ret[mode][key] = unquote(value)
        return ret

    def is_same(self, obj):
        if len(obj) + 2 != len(self.cib) and not self.partial_compare:
            return False
        for key, value in obj.iteritems():
            if key not in self.cib.keys():
                return False
            if key == 'type':
                if str(self.cib.get(key)) not in value:
                    return False
            else:
                if value != self.cib.get(key):
                    return False
        return True


class MonitorParser(BaseParser):
    id_name = "rsc"

    def parse(self, args):
        ret = dict(
            command=args.pop(0),
            rsc=args.pop(0),
            interval=args.pop(0),
            )
        return ret


class GroupParser(BaseParser):
    id_name = "name"

    def parse(self, args):
        ret = dict(
            command=args.pop(0),
            name=args.pop(0),
            rsc=[],
            )
        mode = None
        while (args):
            arg = args.pop(0)
            if (arg in ["params", "meta"]):
                mode = arg
                continue

            if mode is None:
                rsc.append(arg)
                continue

            key, value = arg.split("=")
            if key == "":
                self.module.fail_json(
                    rc=258,
                    msg="no key in key=value option")
            if value == "":
                self.module.fail_json(
                    rc=258,
                    msg="no value in key=value option (key=%s)" % key)
            if mode not in ret:
                ret[mode] = {}
            ret[mode][key] = value

        return ret


class CloneParser(BaseParser):
    id_name = "name"

    def parse(self, args):
        ret = dict(
            command=args.pop(0),
            name=args.pop(0),
            rsc=args.pop(0),
            )
        mode = None
        while (args):
            arg = args.pop(0)
            if (arg in ["params", "meta"]):
                mode = arg
                continue
    
            if mode is None:
                self.module.fail_json(rc=258, msg="no params or meta")
    
            key, value = arg.split("=")
            if key == "":
                self.module.fail_json(
                    rc=258,
                    msg="no key in key=value option")
            if value == "":
                self.module.fail_json(
                    rc=258,
                    msg="no value in key=value option (key=%s)" % key)
            if mode not in ret:
                ret[mode] = {}
            ret[mode][key] = unquote(value)
    
        return ret


class MsParser(CloneParser):
    id_name = "name"


class RscTemplateParser(PrimitiveParser):
    id_name = "name"


class LocationParser(BaseParser):
    id_name = "id"

    def parse(self, args):
        ret = dict(
            command=args.pop(0),
            id=args.pop(0),
            rsc=args.pop(0),
            )
        arg = args.pop(0)
        if arg != "rules":
            ret["score"] = args.pop(0)
            ret["node"] = args.pop(0)
            return ret
        else:
            ret["rules"] = []

        newrule = None
        while (args):
            arg = args.pop(0)

            if arg == "rule":
                if newrule:
                    ret["rules"].append(newrule)
                newrule = dict(expression=[])
                arg = args.pop(0)
                while arg.startswith("$"):
                    key, value = arg.split("=")
                    if value == "":
                        self.module.fail_json(
                            rc=258,
                            msg="no value in key=value option (key=%s)" % key)
                    newargs[key] = value
                    arg = args.pop(0)
                if arg.endswith(":"):
                    newargs["score"] = arg
                else:
                    self.module.fail_json(
                        rc=258,
                        msg="no score in rule for location (id=%s)" % ret["id"])

            elif newrule is not None:
                newrule["expression"].append(args)

            else:
                self.module.fail_json(
                    rc=258,
                    msg="no rule for location (id=%s)" % ret["id"])
        if newrule:
            ret["rules"].append(newrule)

        return ret


class ColocationParser(BaseParser):
    id_name = "id"

    def parse(self, args):
        ret = dict(
            command=args.pop(0),
            id=args.pop(0),
            score=args.pop(0).rstrip(':'),
            rsc=args[:],
            )
        return ret


class OrderParser(BaseParser):
    id_name = "id"

    def parse(self, args):
        ret = dict(
            command=args.pop(0),
            id=args.pop(0),
            kind_or_score=args.pop(0),
            rsc=args[:],
            )
        return ret


class PropertyParser(BaseParser):
    partial_compare = True

    def parse(self, args):
        ret = dict(
            command=args.pop(0),
            )
        if args[0].startswith("$id"):
            args.pop(0)

        if args[0].startswith("cib-bootstrap-options:"):
            args.pop(0)

        while (args):
            arg = args.pop(0)
            if '=' not in arg:
                self.module.fail_json(
                    rc=258,
                    msg="no key-value :%s" % arg)
            key, value = arg.split("=")

            if key == "":
                self.module.fail_json(
                    rc=258,
                    msg="no key in key=value option")
            if value == "":
                self.module.fail_json(
                    rc=258,
                    msg="no value in key=value option (key=%s)" % key)
            ret[key] = unquote(value)

        return ret


class RscDefaultsParser(PropertyParser):
    partial_compare = True


class FencingTopologyParser(BaseParser):
    partial_compare = True

    def parse(self, args):
        ret = dict(
            command=args.pop(0),
            )
        if not args[0].endswith(":"):
            ret["stonith_resources"] = args
            return ret

        newnode = None
        newfence = None
        while(args):
            arg = args.pop(0)
            if arg.endswith(":"):
                if newnode:
                    ret[newnode] = newfence
                newnode = arg
                newfence = []
            else:
                newfence.append(arg)
        if newnode:
            ret[newnode] = newfence
    
        return ret


def splitter(args):
    ret = []
    for a, b, c, d in REX.findall(args):
        if len(b) == 0:
            ret.append(a)
        else:
            ret.append(a+b)
    return ret


class CIBParser(object):

    cib_parser_class = {
        'primitive': PrimitiveParser,
        'monitor': MonitorParser,
        'group': GroupParser,
        'clone': CloneParser,
        'ms': MsParser,
        'rsc_template': RscTemplateParser,
        'location': LocationParser,
        'colocation': ColocationParser,
        'order': OrderParser,
        'property': PropertyParser,
        'rsc_defaults': RscDefaultsParser,
        'fencing_topology': FencingTopologyParser,
        }

    def __init__(self, module):
        self.module = module

    def parse_cib(self, args):
        if args[0] in self.cib_parser_class:
            return self.cib_parser_class[args[0]]\
                    (args[:], module=self.module)
        return None

    def parse_cibs(self, lines):
        cibs = []
        new_line = ""
        for line in lines:
            new_line += line.strip()
            if new_line.endswith('\\'):
                new_line = new_line.rstrip('\\')
            else:
                if len(new_line) == 0:
                    continue
                args = splitter(new_line)
                cib = self.parse_cib(args)
                if cib:
                    cibs.append(cib)
                new_line = ""
        return cibs

class BaseBuilder(object):
    module = None

    def __init__(self, params, module=None):
        if module:
            self.module = module

    def build_command(self, params):
        return self._build_command([], params)

    def _build_command(self, command, params):
        for key,value in params.iteritems():
            if key in ('type', 'rsc'):
                command.insert(0, value)
            elif isinstance(value, dict):
                command.append(key)
                self._build_command(command, value)
            else:
                command.append(key + '="' + str(value) + '"')
        return command

class PrimitiveBuilder(BaseBuilder):

    def build_command(self, params):
        return self._build_command([], params)

    def _build_command(self, command, params, op=False):
        for key,value in params.iteritems():
            if op:
                command.append('op')
            if key in ('type', 'rsc'):
                command.insert(0, value)
            elif isinstance(value, dict):
                if key != 'op':
                    command.append(key)
                self._build_command(command, value, key=='op')
            else:
                command.append(key + '="' + str(value) + '"')
        return command

class ColocationBuilder(BaseBuilder):

    def build_command(self, params):
        return self._build_command([], params)

    def _build_command(self, command, params):
        for key,value in params.iteritems():
            if key=='score':
                command.insert(0, str(value) + ':')
            elif key=='rsc':
                command.extend(value)
        return command

class CommandBuilder(object):

    command_builder_class = {
        'primitive': PrimitiveBuilder,
        'monitor': BaseBuilder,
        'group': BaseBuilder,
        'clone': BaseBuilder,
        'ms': BaseBuilder,
        'rsc_template': BaseBuilder,
        'location': BaseBuilder,
        'colocation': ColocationBuilder,
        'order': BaseBuilder,
        'property': BaseBuilder,
        'rsc_defaults': BaseBuilder,
        'fencing_topology': BaseBuilder,
        }

    def __init__(self, module):
        self.module = module

    def build_command(self, command, params):
        if command in self.command_builder_class:
            builder=self.command_builder_class[command](self.module)
            return builder.build_command(params)
        return None

def main():

    module = AnsibleModule(
        argument_spec = dict(
            state=dict(default='present', choices=['present', 'absent'], type='str'),
            shadow=dict(default=None, type='str'),
            action=dict(required=True, default=None, type='str'),
            name=dict(required=False, default=None, type='str'),
            type=dict(required=False, default=None, type='str'),
            params=dict(default=None, type='dict'),
        ),
        supports_check_mode=True
    )

    state = module.params['state']
    shadow = module.params['shadow']
    action = module.params['action']
    name = module.params['name']
    params = _stringify_dict(module.params['params'])
    changed = False

    if action == None:
        module.fail_json(rc=256, msg="no command given")

    shadowparam=[ '-c', shadow ] if shadow else []

    if action == "commit":
        rc, out, err = module.run_command(["crm"] + shadowparam + ["configure", "commit"])
        if rc:
            module.fail_json(rc=256, msg="crm command failed")
        module.exit_json(args=params, changed=True)

    parser = CIBParser(module)

    crm_args = ["crm"] + shadowparam + ["configure", "show"]
    rc, out, err = module.run_command(crm_args)
    if rc:
        module.fail_json(rc=256, msg="crm command failed")

    is_same = None
    for cur in parser.parse_cibs(out.splitlines()):
        if action != cur.command:
            continue
        if name != cur.id:
            continue
        is_same = cur.is_same(params)
        break

    need_delete = False
    need_stop = True
    need_append = False
    if state == 'absent':
        if is_same is None:
            module.exit_json(args=params, changed=False)
        elif is_same:
            need_delete = True
    else:
        if is_same:
            module.exit_json(args=params, changed=False)
        elif is_same is False and action not in ('property', 'rsc_defaults', 'fencing_topology'):
            need_delete = True
        if action in ('colocation'):
            need_stop = False
        need_append = True

    crm_config_commands = []
    if need_delete:
        if not shadow and need_stop:
            crm_config_commands.append(["resource", "stop", name])
        crm_config_commands.append(["configure", "delete", name])
    if need_append:
        command = ["configure"] + [action]
        if name:
            command.append(name)
        commandbuilder = CommandBuilder(module)
        command+=commandbuilder.build_command(action, params)
        crm_config_commands.append(command)

    for command in crm_config_commands:
        rc, out, err = module.run_command(["crm", "-F"] + shadowparam + command)
        if rc:
            module.fail_json(rc=256, msg="crm command failed \n%s"%command)

    module.exit_json(args=params, changed=True)


# import module snippets
from ansible.module_utils.basic import *

main()
